package com.relax.mybatis.plugin.sensitive.util;

import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.relax.mybatis.annotation.SensitiveField;
import com.relax.mybatis.annotation.SensitiveJSONField;
import com.relax.mybatis.annotation.SensitiveJSONFieldKey;
import com.relax.mybatis.model.FieldCacheProperty;
import com.relax.mybatis.plugin.sensitive.config.SensitiveProperties;
import com.relax.mybatis.plugin.sensitive.strategy.SensitiveContext;
import com.relax.mybatis.plugin.sensitive.strategy.SensitiveFieldHolder;
import com.relax.mybatis.util.PluginUtil;

import lombok.extern.slf4j.Slf4j;

/**
 * @author relax
 * @date 2022/07/05
 * @desc 脱敏工具类
 */
@Slf4j
public class SensitiveUtil {

    public static Object handleStatementHandler(Invocation invocation,
                                                BiConsumer<MetaObject, FieldCacheProperty> consumer) throws Throwable {
        StatementHandler statementHandler = PluginUtil.getRealTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        SqlCommandType commandType = mappedStatement.getSqlCommandType();

        if (PluginUtil.isWriteCommand(commandType)) {
            Configuration configuration = mappedStatement.getConfiguration();
            BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
            Object object = boundSql.getParameterObject();
            if (null != object) {
                if (object instanceof Map) {
                    Map map = (Map) object;
                    for (Object key : map.keySet()) {
                        Object value = map.get(key);
                        if (null != value) {
                            if (value instanceof java.util.List) {
                                if (((String) key).startsWith("param"))
                                    continue;
                                List list = (List) value;
                                for (Object item : list) {
                                    if (SensitiveFieldHolder.handleObject(configuration, item, consumer))
                                        ;
                                }
                                continue;
                            }
                            if (SensitiveFieldHolder.handleObject(configuration, value, consumer))
                                break;
                        }
                    }
                } else {
                    SensitiveFieldHolder.handleObject(configuration, object, consumer);
                }
            }
        }
        return invocation.proceed();
    }

    /**
     * 脱敏
     *
     * @param sensitiveProperties
     * @param metaObject
     * @param fieldCacheProperty
     * @param withWrite
     */
    public static void doSensitive(SensitiveProperties sensitiveProperties,
                                   MetaObject metaObject,
                                   FieldCacheProperty fieldCacheProperty,
                                   boolean withWrite) {
        String name = fieldCacheProperty.getFieldName();
        SensitiveField sensitiveField = fieldCacheProperty.getSensitiveField();
        SensitiveJSONField sensitiveJSONField = fieldCacheProperty.getSensitiveJSONField();
        if (null != sensitiveField && withWrite == sensitiveField.withWrite()) {
            Object object = metaObject.getValue(name);
            if (null != object && object instanceof String) {
                try {
                    String symbol = sensitiveProperties.getSymbol(sensitiveField);
                    object = SensitiveContext.getStrategy(sensitiveField.type()).handle((String) object, symbol);
                } catch (Exception e) {
                    log.error("handle sensitive field {}.{} occur exception: {}",
                            metaObject.getOriginalObject().getClass().getTypeName(), name, e.getMessage());
                }
                metaObject.setValue(name, object);
            }
        } else if (null != sensitiveJSONField) {
            Object object = metaObject.getValue(name);
            if (null != object && object instanceof String) {
                try {
                    object = processJsonField(sensitiveProperties, sensitiveJSONField, withWrite, (String) object);
                } catch (Exception e) {
                    log.error("handle sensitive field {}.{} occur exception: {}",
                            metaObject.getOriginalObject().getClass().getTypeName(), name, e.getMessage());
                }
                metaObject.setValue(name, object);
            }
        }
    }

    /**
     * 处理json字段
     *
     * @param sensitiveProperties
     * @param sensitiveJSONField
     * @param withWrite
     * @param value
     * @return
     */
    public static String processJsonField(SensitiveProperties sensitiveProperties,
                                          SensitiveJSONField sensitiveJSONField,
                                          boolean withWrite, String value) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            Map<String, Object> map = mapper.readValue(value.toString(), Map.class);
            SensitiveJSONFieldKey[] keys = sensitiveJSONField.keys();
            for (SensitiveJSONFieldKey sensitiveJSONFieldKey : keys) {
                if (withWrite == sensitiveJSONFieldKey.withWrite()) {
                    String key = sensitiveJSONFieldKey.value();
                    String type = sensitiveJSONFieldKey.type();
                    Object oldData = map.get(key);
                    if (oldData != null) {
                        String symbol = sensitiveProperties.getSymbol(sensitiveJSONFieldKey);
                        String newData = SensitiveContext.getStrategy(type).handle(oldData.toString(), symbol);
                        map.put(key, newData);
                    }
                }
            }
            return mapper.writeValueAsString(map);
        } catch (Throwable e) {
            log.error("脱敏json串时失败，cause : {}", e.getMessage(), e);
            return value;
        }
    }

    public static Object handleResultSet(Invocation invocation,
                                         BiConsumer<MetaObject, FieldCacheProperty> consumer) throws Throwable {
        List<Object> list = (List<Object>) invocation.proceed();
        if (list.isEmpty())
            return list;
        Object target = invocation.getTarget();
        while (target instanceof Proxy) {
            Plugin plugin = (Plugin) Proxy.getInvocationHandler(target);
            Field field = plugin.getClass().getDeclaredField("target");
            field.setAccessible(true);
            target = field.get(plugin);
        }
        if (target instanceof DefaultResultSetHandler) {
            DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) target;
            Field field = defaultResultSetHandler.getClass().getDeclaredField("mappedStatement");
            field.setAccessible(true);
            MappedStatement mappedStatement = (MappedStatement) field.get(defaultResultSetHandler);
            Configuration configuration = mappedStatement.getConfiguration();
            for (Object object : list) {
                if (null != object && !SensitiveFieldHolder.handleObject(configuration, object, consumer))
                    break;
            }
        }
        return list;
    }

}
